// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Actors;
using System.Threading.Actors.Utilities;
using System.Threading.Tasks;

namespace TestActorMethods
{
    //TODO:
    //  Put multi-partition handling into PersistenceHelper (use sbh.PermaStore instead of dictionary)
    //  (or maybe PermaStore AND dictionary)
    //  Test Get, TryGet, Set, Remove across multiple partitions, before/after flush
    internal class PersistenceHelper : IPersistenceHandler
    {
        private static readonly Task s_preCompletedTask = Task.FromResult(true);

        public object Get(string partitionName, string key) 
        {
            return PermaStore[partitionName][key];
        }

        public bool TryGet(string partitionName, string key, out object value) 
        {
            ConcurrentDictionary<string, object> partitionStorage;
            if (PermaStore.TryGetValue(partitionName, out partitionStorage))
            {
                return partitionStorage.TryGetValue(key, out value);
            }
            else
            {
                value = null;
                return false;
            }
        }

        public ConcurrentDictionary<string, ConcurrentDictionary<string, object>> PermaStore { get; set; }

        public Task OnPersistAsync(IList<ChangeOperation> toPersist)
        {
            foreach (var change in toPersist)
            {
                var partitionName = change.PartitionName;
                var partitionStore = PermaStore.GetOrAdd(partitionName, x => new ConcurrentDictionary<string, object>());
                object unused;
                if (change.OperationType == ChangeOperationType.Set)
                {
                    partitionStore[change.OperationKey] = change.OperationValue;
                }
                else if (change.OperationType == ChangeOperationType.Remove)
                {
                    partitionStore.TryRemove(change.OperationKey, out unused);
                }
                else throw new Exception("Unknown operation type " + change.OperationType);
            }
            return s_preCompletedTask;
        }
    }

    [TestClass]
    public class StateBufferingHelperUnitTests
    {
        [TestMethod]
        public void StateBufferingHelper_Basics()
        {
            PersistenceHelper ph = new PersistenceHelper();
            StateBufferingHelper sbh = new StateBufferingHelper(ph);
            ph.PermaStore = sbh.PermaStore;
            
            // See that Set is handled correctly
            sbh.Set("foo", "one", 1);

            object temp;
            Assert.IsFalse(ph.TryGet("foo", "one", out temp), "Did not expect Set() to be persisted before Flush");
            Assert.IsTrue(sbh.TryGet("foo", "one", out temp), "Expected Set() to be effected in sbh before Flush");
            Assert.IsTrue(sbh.ChangesPendingForCurrentOperation("foo"), "Expected Set() changes to be pending in sbh before Flush");
            sbh.Flush("foo").Wait();
            Assert.IsFalse(sbh.ChangesPendingForCurrentOperation("foo"), "Did not expect Set() changes to be pending in sbh after Flush");
            Assert.IsTrue(ph.TryGet("foo", "one", out temp), "Expected Set() to be persisted after Flush");
            Assert.AreEqual<int>((int)temp, 1, "Expected \"one\" to be 1 after Flush");
            Assert.IsTrue(sbh.TryGet("foo", "one", out temp), "Expected Set() to be effected in sbh after Flush");

            // See that Remove is handled correctly
            sbh.Remove("foo", "one");

            Assert.IsTrue(ph.TryGet("foo", "one", out temp), "Did not expect Remove() to be persisted before Flush");
            Assert.IsFalse(sbh.TryGet("foo", "one", out temp), "Expected Remove() to be effected in sbh before Flush");
            Assert.IsTrue(sbh.ChangesPendingForCurrentOperation("foo"), "Expected Remove() changes to be pending in sbh before Flush");
            sbh.Flush("foo").Wait();
            Assert.IsFalse(sbh.ChangesPendingForCurrentOperation("foo"), "Did not expect Remove() changes to be pending in sbh before Flush");
            Assert.IsFalse(ph.TryGet("foo", "one", out temp), "Expected Remove() to be persisted after Flush");
            Assert.IsFalse(sbh.TryGet("foo", "one", out temp), "Expected Remove() to be effected in sbh after Flush");

            // See that key-value pairs from separate partition names are properly isolated
            sbh.Set("foo", "k1", 42);
            sbh.Set("bar", "k1", 13);
            Assert.IsTrue(sbh.TryGet("foo", "k1", out temp), "Expected Set('foo', 'k1') to be effected in sbh before Flush");
            Assert.AreEqual<int>((int)temp, 42, "Expected sbh.TryGet('foo','k1') to return 42, before Flush");
            Assert.IsTrue(sbh.TryGet("bar", "k1", out temp), "Expected Set('bar', 'k1') to be effected in sbh before Flush");
            Assert.AreEqual<int>((int)temp, 13, "Expected sbh.TryGet('foo','k1') to return 13, before Flush");
            sbh.Flush("foo").Wait();
            Assert.IsTrue(ph.TryGet("foo", "k1", out temp), "Expected Set('foo', 'k1') to be persisted after Flush('foo')");
            Assert.AreEqual<int>((int)temp, 42, "Expected ph.TryGet('foo','k1') to return 42, after Flush('foo')");
            Assert.IsFalse(ph.TryGet("bar", "k1", out temp), "Did not expect Set('bar', 'k1') to be persisted after Flush('foo')");
            sbh.Flush("bar").Wait();
            Assert.IsTrue(ph.TryGet("bar", "k1", out temp), "Expected Set('bar', 'k1') to be persisted after Flush('bar')");
            Assert.AreEqual<int>((int)temp, 13, "Expected ph.TryGet('bar','k1') to return 13, after Flush('bar')");
        }

        [TestMethod]
        public void StateBufferingHelper_IsolatedState()
        {
            PersistenceHelper ph = new PersistenceHelper();
            StateBufferingHelper sbh = new StateBufferingHelper(ph);
            ph.PermaStore = sbh.PermaStore;

            // null IActorInteractor OK since we are not interacting with other actors
            IsolatedActorState ias1 = new IsolatedActorState(sbh, "Actor1", "fabric:/actor/Actor1", new ActorETWEventListener("fabric:/actor/Actor1"), null); 
            IsolatedActorState ias2 = new IsolatedActorState(sbh, "Actor2", "fabric:/actor/Actor2", new ActorETWEventListener("fabric:/actor/Actor2"), null);

            // See that Set is handled correctly
            ias1.Set("one", 1);

            object temp;
            Assert.IsFalse(ph.TryGet("Actor1", "one", out temp), "Did not expect Set() to be persisted before Flush");
            Assert.IsTrue(ias1.TryGet("one", out temp), "Expected Set() to be effected in ias1 before Flush");
            ias1.Flush().Wait();
            Assert.IsTrue(ph.TryGet("Actor1", "one", out temp), "Expected Set() to be persisted after Flush");
            Assert.AreEqual<int>((int)temp, 1, "Expected \"one\" to be 1 after Flush");
            Assert.IsTrue(ias1.TryGet("one", out temp), "Expected Set() to be effected in ias1 after Flush");

            // See that Remove is handled correctly
            ias1.Remove("one");

            Assert.IsTrue(ph.TryGet("Actor1", "one", out temp), "Did not expect Remove() to be persisted before Flush");
            Assert.IsFalse(ias1.TryGet("one", out temp), "Expected Remove() to be effected in ias1 before Flush");
            ias1.Flush().Wait();
            Assert.IsFalse(ph.TryGet("Actor1", "one", out temp), "Expected Remove() to be persisted after Flush");
            Assert.IsFalse(ias1.TryGet("one", out temp), "Expected Remove() to be effected in ias1 after Flush");

            // See that key-value pairs from separate partition names are properly isolated
            ias1.Set("k1", 42);
            ias2.Set("k1", 13);
            Assert.IsTrue(ias1.TryGet("k1", out temp), "Expected Set('k1') to be effected in ias1 before Flush");
            Assert.AreEqual<int>((int)temp, 42, "Expected ias1.TryGet('k1') to return 42, before Flush");
            Assert.IsTrue(ias2.TryGet("k1", out temp), "Expected Set('k1') to be effected in ias2 before Flush");
            Assert.AreEqual<int>((int)temp, 13, "Expected ias2.TryGet('k1') to return 13, before Flush");
            ias1.Flush().Wait();
            Assert.IsTrue(ph.TryGet("Actor1", "k1", out temp), "Expected ias1.Set('k1') to be persisted after ias1.Flush()");
            Assert.AreEqual<int>((int)temp, 42, "Expected ph.TryGet('Actor1','k1') to return 42, after ias1.Flush()");
            Assert.IsFalse(ph.TryGet("Actor2", "k1", out temp), "Did not expect ias2.Set('k1') to be persisted after ias1.Flush()");
            ias2.Flush().Wait();
            Assert.IsTrue(ph.TryGet("Actor2", "k1", out temp), "Expected ias2.Set('k1') to be persisted after ias2.Flush()");
            Assert.AreEqual<int>((int)temp, 13, "Expected ph.TryGet('Actor2','k1') to return 13, after ias2.Flush()");
        }

    }
}
